#!/usr/bin/python3

# SPDX-FileCopyrightText: Red Hat, Inc.
# SPDX-License-Identifier: GPL-2.0-or-later

from __future__ import absolute_import

import json
import os
import tempfile
import time

from vdsm.common import cmdutils
from vdsm.storage import constants as sc
from vdsm.storage import qemuimg

import hooking

# Name of vg used for lvm based local disks
OVIRT_LOCAL_VG = "ovirt-local"

# The thin pool name in case of thin LV
LOCAL_POOL = "pool0"

# Helper to execute LVM command as super user
HELPER = "/usr/libexec/vdsm/localdisk-helper"

# The LV/local disk is created with the UPDATING tag.
# This tag will be cleared, once the copying of the data from the template
# is finished.
# When the hook is called, and the tag is present on an existing LV, it means
# that the copy data process failed. In that case, the LV will be removed,
# then recreated and the copy process will run again.
TAG_UPDATING = "UPDATING"

# The LV is created with a tag containing the VM id with 'VM_' prefix.
# This tag can be helpful to locate LV of VMs that have deleted, so that
# the operator will be able to delete the unused LVs/disks.
TAG_PREFIX_VM = "VM_"

# The LV/local disk are copied as RAW format for best performance.
LOCAL_FORMAT = "raw"

# LV/local are block devices, but the original drive may be a 'file' or
# 'network' disk.
LOCAL_DISK_TYPE = "block"

# Supported backends
BACKEND_LVM = "lvm"
BACKEND_LVM_THIN = "lvmthin"

# Directory used for temporary files when converting images.
# The file system mounted here must support directio I/O.
TMP_DIR = "/var/tmp"


class NoSuchLV(Exception):
    """ Raised when lv does not exists """


class LVIsUpdating(Exception):
    """ Raised when lv has an UPDATING """


def main():
    backend = os.environ.get('localdisk')
    if backend is None:
        return
    if backend not in [BACKEND_LVM, BACKEND_LVM_THIN]:
        hooking.log("localdisk-hook: unsupported backend: %r" % backend)
        return
    thin = backend == BACKEND_LVM_THIN
    disk = hooking.read_json()
    replace_disk(disk, thin)
    hooking.write_json(disk)


def replace_disk(disk, thin):
    vm_id = os.environ['vmId']
    orig_path = disk["path"]
    img_id = disk["imageID"]
    lv_name = disk["volumeID"]
    src_format = "qcow2" if disk["format"] == "cow" else disk["format"]

    try:
        lv_info(lv_name)
    except NoSuchLV:
        hooking.log("localdisk-hook: local disk not found, "
                    "creating logical volume (name=%s)" % lv_name)
        create_local_disk(orig_path, lv_name, vm_id, img_id, src_format, thin)
    except LVIsUpdating:
        hooking.log("localdisk-hook: found unfinished disk, "
                    "recreating logical volume (name=%s)" % lv_name)
        remove_local_disk(lv_name)
        create_local_disk(orig_path, lv_name, vm_id, img_id, src_format, thin)
    else:
        hooking.log("localdisk-hook: reusing local disk " + lv_name)
        activate_lv(lv_name)

    disk["path"] = lv_path(lv_name)
    disk["format"] = LOCAL_FORMAT
    disk["diskType"] = LOCAL_DISK_TYPE


def create_local_disk(orig_path, lv_name, vm_id, img_id, src_format, thin):
    res = qemuimg.info(orig_path)
    size = res["virtual-size"]
    hooking.log("localdisk-hook: creating logical volume (name=%s, size=%d, "
                "thin=%s)"
                % (lv_name, size, thin))
    create_lv(lv_name, size, vm_id, img_id, thin)
    dst_path = lv_path(lv_name)
    start = time.time()
    hooking.log("localdisk-hook: copying image %s to %s"
                % (orig_path, dst_path))
    if thin:
        convert_image_thin(orig_path, src_format, dst_path)
    else:
        convert_image(orig_path, src_format, dst_path)
    elapsed = time.time() - start
    hooking.log("localdisk-hook: copy completed in %.3f seconds" % (elapsed,))
    delete_lv_tag(lv_name, TAG_UPDATING)


def remove_local_disk(lv_name):
    helper("remove", lv_name)


# Copying images

def convert_image_thin(src_path, src_format, dst_path):
    # We copy in two steps:
    # 1. Convert image to raw sparse file on local storage
    # 2. Write sparse file to lv, keeping sparseness
    # On files systems supporting sparsness (e.g. NFS 4.2), this is also the
    # fastest way to copy from shared storage. On other file systems, copying
    # raw imaged directly may be faster.
    with tempfile.NamedTemporaryFile(dir=TMP_DIR, prefix="localdisk.") as tmp:
        convert_image(src_path, src_format, tmp.name)
        copy_sparse(tmp.name, dst_path)


def convert_image(src_path, src_format, dst_path):
    operation = qemuimg.convert(src_path,
                                dst_path,
                                srcFormat=src_format,
                                dstFormat=qemuimg.FORMAT.RAW)
    operation.run()


def copy_sparse(src_path, dst_path):
    cmd = ["/usr/bin/dd",
           "if=" + src_path,
           "of=" + dst_path,
           "bs=1M",
           "iflag=direct",
           "oflag=direct",
           "conv=sparse"]
    rc, out, err = hooking.execCmd(cmd, raw=True)
    if rc != 0:
        raise cmdutils.Error(cmd=cmd, rc=rc, out=out, err=err)


# LVM helper operation

def lv_info(lv_name):
    out = helper("list", lv_name)
    lvs = json.loads(out)["report"][0]["lv"]
    if not lvs:
        raise NoSuchLV
    lv = lvs[0]
    if TAG_UPDATING in lv["lv_tags"].split(","):
        raise LVIsUpdating
    return lv


def delete_lv_tag(lv_name, tag):
    helper("change", "--deltag", tag, lv_name)


def activate_lv(lv_name):
    helper("change", "--activate", "y", lv_name)


def lv_path(name):
    return os.path.join("/dev", OVIRT_LOCAL_VG, name)


def create_lv(lv_name, size, vm_id, img_id, thin):
    cmd = ["create",
           "--addtag", TAG_UPDATING,
           "--addtag", TAG_PREFIX_VM + vm_id,
           "--addtag", sc.TAG_PREFIX_IMAGE + img_id,
           "--addtag", sc.TAG_PREFIX_PARENT + sc.BLANK_UUID]
    if thin:
        cmd.extend(("--thinpool", LOCAL_POOL))
        cmd.extend(("--virtualsize", str(size) + "b"))
    else:
        cmd.extend(("--size", str(size) + "b"))
    cmd.append(lv_name)
    helper(*cmd)


def helper(*args):
    cmd = [HELPER]
    cmd.extend(args)
    rc, out, err = hooking.execCmd(cmd, sudo=True, raw=True)
    if rc != 0:
        raise cmdutils.Error(cmd=HELPER, rc=rc, out=out, err=err)
    return out


main()
